Brain view: slower edge falloff, chase arc in the neuron's gap

--brain-decay 90s -> 150s and the wire floor 32% -> 16%, so a quiet
connection dims further and takes longer doing it. The pulse ring's inset
and band become custom properties (defaults unchanged for the editor's
card), and the neuron passes its own ring and gap so the arc runs the full
width of the gap; it moves after the disc, which otherwise paints over it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZeGnqVsf5VHQqvz4HdUhN
This commit is contained in:
2026-08-23 06:21:26 +02:00
co-authored by Claude Opus 5
parent b97aed7393
commit a4cb6a1bab
2 changed files with 35 additions and 14 deletions
+28 -9
View File
@@ -1,5 +1,5 @@
import { Handle, type NodeProps, Position } from "@xyflow/react" import { Handle, type NodeProps, Position } from "@xyflow/react"
import { memo, useEffect, useRef, useState } from "react" import { type CSSProperties, memo, useEffect, useRef, useState } from "react"
import { duration } from "@/lib/motion" import { duration } from "@/lib/motion"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@@ -72,14 +72,16 @@ function BrainNodeComponent({ data }: NodeProps) {
return () => clearTimeout(timer) return () => clearTimeout(timer)
}, [emits]) }, [emits])
// The neuron's two bands in pixels: the ring, and the gap the arc runs in.
const ring = Math.round(size * RING)
const gap = Math.round(size * GAP)
return ( return (
// The pulse ring measures itself from here rather than from the neuron, so // The pulse ring measures itself from here rather than from the neuron:
// the thick status ring stays clear of it — see `.node-pulse` in flow.css. // `inset` on an absolute child resolves against the padding box, so an arc
// measured from the neuron would start inside its border rather than at the
// rim — see `.node-pulse` in flow.css.
<span className="relative flex rounded-full"> <span className="relative flex rounded-full">
{/* Remounting on each emit is what restarts the animation, so this is
gated on the pulse rather than on the count: a neuron mounting with a
count already in the store would otherwise play it once for free. */}
{firing ? <span key={emits} className="node-pulse" /> : null}
<span <span
className={cn( className={cn(
"brain-cell relative flex items-center justify-center rounded-full", "brain-cell relative flex items-center justify-center rounded-full",
@@ -96,8 +98,8 @@ function BrainNodeComponent({ data }: NodeProps) {
style={{ style={{
width: size, width: size,
height: size, height: size,
borderWidth: Math.round(size * RING), borderWidth: ring,
boxShadow: `inset 0 0 0 ${Math.round(size * GAP)}px var(--brain-gap)`, boxShadow: `inset 0 0 0 ${gap}px var(--brain-gap)`,
}} }}
title={`${label} · ${kind} · ${members.join(", ")}${issue ? ` · ${issue}` : ""}${failure ? ` · ${failure}` : ""}`} title={`${label} · ${kind} · ${members.join(", ")}${issue ? ` · ${issue}` : ""}${failure ? ` · ${failure}` : ""}`}
> >
@@ -153,6 +155,23 @@ function BrainNodeComponent({ data }: NodeProps) {
) : null} ) : null}
</span> </span>
</span> </span>
{/* Remounting on each emit is what restarts the animation, so this is
gated on the pulse rather than on the count: a neuron mounting with a
count already in the store would otherwise play it once for free.
After the neuron rather than before it: the arc runs in the gap the
neuron paints, and an earlier sibling would be painted over. */}
{firing ? (
<span
key={emits}
className="node-pulse"
style={
{
"--node-pulse-inset": `${ring}px`,
"--node-pulse-band": `${gap}px`,
} as CSSProperties
}
/>
) : null}
</span> </span>
) )
} }
+7 -5
View File
@@ -133,13 +133,15 @@
* is what lets one rule fit both a rounded card and a circle. * is what lets one rule fit both a rounded card and a circle.
* *
* The band is the padding: the content box is cut back out of the mask, which * The band is the padding: the content box is cut back out of the mask, which
* leaves a ring rather than the filled shape the gradient paints. * leaves a ring rather than the filled shape the gradient paints. Both are
* overridable, for a node whose own bands say where the arc belongs — the
* brain's neuron runs it in the gap between its ring and its disc.
*/ */
.node-pulse { .node-pulse {
position: absolute; position: absolute;
inset: -4px; inset: var(--node-pulse-inset, -4px);
border-radius: inherit; border-radius: inherit;
padding: 2px; padding: var(--node-pulse-band, 2px);
pointer-events: none; pointer-events: none;
background: conic-gradient( background: conic-gradient(
from var(--node-pulse-angle), from var(--node-pulse-angle),
@@ -243,7 +245,7 @@
*/ */
.react-flow.brain-flat { .react-flow.brain-flat {
--xy-background-color: transparent; --xy-background-color: transparent;
--brain-decay: 90s; --brain-decay: 150s;
/* /*
* Thicker than the editor's, to match the weight the mark draws them at — and * Thicker than the editor's, to match the weight the mark draws them at — and
* the pulse widens with it, or a message would leave its edge looking thinner * the pulse widens with it, or a message would leave its edge looking thinner
@@ -381,7 +383,7 @@
* from another. * from another.
*/ */
.brain-wire { .brain-wire {
--brain-ink: color-mix(in srgb, var(--foreground) 32%, transparent); --brain-ink: color-mix(in srgb, var(--foreground) 16%, transparent);
} }
.brain-wire.brain-idle { .brain-wire.brain-idle {