feat(viz): tree/BFS/DFS patterns
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
'use client';
|
||||
|
||||
import { useVisualization } from '@/lib/visualizations/use-visualization';
|
||||
import type { AlgorithmDefinition } from '@/lib/visualizations/types';
|
||||
import { VisualizationContainer } from '../core/visualization-container';
|
||||
import { BinaryTreeView } from '../data-structures/binary-tree-view';
|
||||
import { QueueView } from '../data-structures/queue-view';
|
||||
import { ArrayView } from '../data-structures/array-view';
|
||||
|
||||
interface BFSVisualizationProps {
|
||||
algorithm: AlgorithmDefinition;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function BFSVisualization({
|
||||
algorithm,
|
||||
className,
|
||||
}: BFSVisualizationProps) {
|
||||
const {
|
||||
currentStep,
|
||||
currentStepIndex,
|
||||
totalSteps,
|
||||
playback,
|
||||
controls,
|
||||
currentPhase,
|
||||
progress,
|
||||
} = useVisualization(algorithm);
|
||||
|
||||
const { dataState } = currentStep;
|
||||
const tree = dataState.trees?.[0] ?? null;
|
||||
const bfsQueue = dataState.queues?.[0] ?? null;
|
||||
const outputArray = dataState.arrays?.[0] ?? null;
|
||||
|
||||
// Find the current node ID based on node states
|
||||
const currentNodeId = tree?.nodes.find((n) => n.state === 'current')?.id;
|
||||
|
||||
return (
|
||||
<VisualizationContainer
|
||||
title={algorithm.title}
|
||||
pattern={algorithm.pattern}
|
||||
code={algorithm.code}
|
||||
currentLine={currentStep.codeLine}
|
||||
highlightLines={currentStep.codeHighlightLines}
|
||||
explanation={currentStep.explanation}
|
||||
decision={currentStep.decision}
|
||||
phase={currentPhase}
|
||||
variables={dataState.variables}
|
||||
currentStepIndex={currentStepIndex}
|
||||
totalSteps={totalSteps}
|
||||
isPlaying={playback.isPlaying}
|
||||
speed={playback.speed}
|
||||
controls={controls}
|
||||
progress={progress}
|
||||
className={className}
|
||||
>
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Top: Binary tree */}
|
||||
{tree && (
|
||||
<BinaryTreeView
|
||||
tree={tree}
|
||||
currentNodeId={currentNodeId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Bottom: Queue + Output */}
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
{/* BFS Queue */}
|
||||
{bfsQueue && (
|
||||
<QueueView queue={bfsQueue} />
|
||||
)}
|
||||
|
||||
{/* Output array */}
|
||||
{outputArray && (
|
||||
<ArrayView
|
||||
array={outputArray}
|
||||
pointers={dataState.pointers}
|
||||
elementSize="sm"
|
||||
showIndices={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</VisualizationContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
'use client';
|
||||
|
||||
import { useVisualization } from '@/lib/visualizations/use-visualization';
|
||||
import type { AlgorithmDefinition } from '@/lib/visualizations/types';
|
||||
import { VisualizationContainer } from '../core/visualization-container';
|
||||
import { BinaryTreeView } from '../data-structures/binary-tree-view';
|
||||
import { StackView } from '../data-structures/stack-view';
|
||||
import { ArrayView } from '../data-structures/array-view';
|
||||
|
||||
interface DFSVisualizationProps {
|
||||
algorithm: AlgorithmDefinition;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function DFSVisualization({
|
||||
algorithm,
|
||||
className,
|
||||
}: DFSVisualizationProps) {
|
||||
const {
|
||||
currentStep,
|
||||
currentStepIndex,
|
||||
totalSteps,
|
||||
playback,
|
||||
controls,
|
||||
currentPhase,
|
||||
progress,
|
||||
} = useVisualization(algorithm);
|
||||
|
||||
const { dataState } = currentStep;
|
||||
const tree = dataState.trees?.[0] ?? null;
|
||||
const dfsStack = dataState.stacks?.[0] ?? null;
|
||||
const outputArray = dataState.arrays?.[0] ?? null;
|
||||
|
||||
// Find the current node ID based on node states
|
||||
const currentNodeId = tree?.nodes.find((n) => n.state === 'current')?.id;
|
||||
|
||||
return (
|
||||
<VisualizationContainer
|
||||
title={algorithm.title}
|
||||
pattern={algorithm.pattern}
|
||||
code={algorithm.code}
|
||||
currentLine={currentStep.codeLine}
|
||||
highlightLines={currentStep.codeHighlightLines}
|
||||
explanation={currentStep.explanation}
|
||||
decision={currentStep.decision}
|
||||
phase={currentPhase}
|
||||
variables={dataState.variables}
|
||||
currentStepIndex={currentStepIndex}
|
||||
totalSteps={totalSteps}
|
||||
isPlaying={playback.isPlaying}
|
||||
speed={playback.speed}
|
||||
controls={controls}
|
||||
progress={progress}
|
||||
className={className}
|
||||
>
|
||||
<div className="flex items-start gap-8">
|
||||
{/* Left: Binary tree */}
|
||||
{tree && (
|
||||
<BinaryTreeView
|
||||
tree={tree}
|
||||
currentNodeId={currentNodeId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Right side: Stack + Output */}
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* DFS Stack */}
|
||||
{dfsStack && (
|
||||
<StackView stack={dfsStack} />
|
||||
)}
|
||||
|
||||
{/* Output array */}
|
||||
{outputArray && (
|
||||
<ArrayView
|
||||
array={outputArray}
|
||||
pointers={dataState.pointers}
|
||||
elementSize="sm"
|
||||
showIndices={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</VisualizationContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
'use client';
|
||||
|
||||
import { useVisualization } from '@/lib/visualizations/use-visualization';
|
||||
import type { AlgorithmDefinition } from '@/lib/visualizations/types';
|
||||
import { VisualizationContainer } from '../core/visualization-container';
|
||||
import { BinaryTreeView } from '../data-structures/binary-tree-view';
|
||||
import { StackView } from '../data-structures/stack-view';
|
||||
import { ArrayView } from '../data-structures/array-view';
|
||||
|
||||
interface TreeTraversalVisualizationProps {
|
||||
algorithm: AlgorithmDefinition;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function TreeTraversalVisualization({
|
||||
algorithm,
|
||||
className,
|
||||
}: TreeTraversalVisualizationProps) {
|
||||
const {
|
||||
currentStep,
|
||||
currentStepIndex,
|
||||
totalSteps,
|
||||
playback,
|
||||
controls,
|
||||
currentPhase,
|
||||
progress,
|
||||
} = useVisualization(algorithm);
|
||||
|
||||
const { dataState } = currentStep;
|
||||
const tree = dataState.trees?.[0] ?? null;
|
||||
const traversalStack = dataState.stacks?.[0] ?? null;
|
||||
const outputArray = dataState.arrays?.[0] ?? null;
|
||||
|
||||
// Find the current node ID based on node states
|
||||
const currentNodeId = tree?.nodes.find((n) => n.state === 'current')?.id;
|
||||
|
||||
return (
|
||||
<VisualizationContainer
|
||||
title={algorithm.title}
|
||||
pattern={algorithm.pattern}
|
||||
code={algorithm.code}
|
||||
currentLine={currentStep.codeLine}
|
||||
highlightLines={currentStep.codeHighlightLines}
|
||||
explanation={currentStep.explanation}
|
||||
decision={currentStep.decision}
|
||||
phase={currentPhase}
|
||||
variables={dataState.variables}
|
||||
currentStepIndex={currentStepIndex}
|
||||
totalSteps={totalSteps}
|
||||
isPlaying={playback.isPlaying}
|
||||
speed={playback.speed}
|
||||
controls={controls}
|
||||
progress={progress}
|
||||
className={className}
|
||||
>
|
||||
<div className="flex items-start gap-8">
|
||||
{/* Left: Binary tree */}
|
||||
{tree && (
|
||||
<BinaryTreeView
|
||||
tree={tree}
|
||||
currentNodeId={currentNodeId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Right side: Stack + Output */}
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Traversal stack */}
|
||||
{traversalStack && (
|
||||
<StackView stack={traversalStack} />
|
||||
)}
|
||||
|
||||
{/* Output array */}
|
||||
{outputArray && (
|
||||
<ArrayView
|
||||
array={outputArray}
|
||||
pointers={dataState.pointers}
|
||||
elementSize="sm"
|
||||
showIndices={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</VisualizationContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { BinaryTreeState, BinaryTreeNodeState } from '@/lib/visualizations/types';
|
||||
import { TreeNode } from '../primitives/tree-node';
|
||||
|
||||
interface BinaryTreeViewProps {
|
||||
tree: BinaryTreeState;
|
||||
currentNodeId?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const NODE_RADIUS = 24;
|
||||
const LEVEL_HEIGHT = 72;
|
||||
const MIN_NODE_SPACING = 56;
|
||||
const SVG_PADDING = 32;
|
||||
|
||||
interface NodePosition {
|
||||
node: BinaryTreeNodeState;
|
||||
x: number;
|
||||
y: number;
|
||||
parentX?: number;
|
||||
parentY?: number;
|
||||
}
|
||||
|
||||
function buildNodeMap(nodes: BinaryTreeNodeState[]): Map<string, BinaryTreeNodeState> {
|
||||
const map = new Map<string, BinaryTreeNodeState>();
|
||||
for (const node of nodes) {
|
||||
map.set(node.id, node);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function calculateTreeDepth(
|
||||
nodeId: string | null,
|
||||
nodeMap: Map<string, BinaryTreeNodeState>
|
||||
): number {
|
||||
if (!nodeId) return 0;
|
||||
const node = nodeMap.get(nodeId);
|
||||
if (!node) return 0;
|
||||
const leftDepth = calculateTreeDepth(node.left, nodeMap);
|
||||
const rightDepth = calculateTreeDepth(node.right, nodeMap);
|
||||
return 1 + Math.max(leftDepth, rightDepth);
|
||||
}
|
||||
|
||||
function calculatePositions(
|
||||
tree: BinaryTreeState,
|
||||
totalWidth: number
|
||||
): NodePosition[] {
|
||||
const nodeMap = buildNodeMap(tree.nodes);
|
||||
const positions: NodePosition[] = [];
|
||||
|
||||
function traverse(
|
||||
nodeId: string | null,
|
||||
level: number,
|
||||
left: number,
|
||||
right: number,
|
||||
parentX?: number,
|
||||
parentY?: number
|
||||
) {
|
||||
if (!nodeId) return;
|
||||
const node = nodeMap.get(nodeId);
|
||||
if (!node) return;
|
||||
|
||||
const x = (left + right) / 2;
|
||||
const y = SVG_PADDING + level * LEVEL_HEIGHT + NODE_RADIUS;
|
||||
|
||||
positions.push({ node, x, y, parentX, parentY });
|
||||
|
||||
const childWidth = (right - left) / 2;
|
||||
traverse(node.left, level + 1, left, left + childWidth, x, y);
|
||||
traverse(node.right, level + 1, left + childWidth, right, x, y);
|
||||
}
|
||||
|
||||
traverse(tree.rootId, 0, 0, totalWidth);
|
||||
return positions;
|
||||
}
|
||||
|
||||
export function BinaryTreeView({
|
||||
tree,
|
||||
currentNodeId,
|
||||
className,
|
||||
}: BinaryTreeViewProps) {
|
||||
const { positions, svgWidth, svgHeight } = useMemo(() => {
|
||||
const nodeMap = buildNodeMap(tree.nodes);
|
||||
const depth = calculateTreeDepth(tree.rootId, nodeMap);
|
||||
const maxNodesAtBottom = Math.pow(2, depth - 1);
|
||||
const width = Math.max(
|
||||
maxNodesAtBottom * MIN_NODE_SPACING + SVG_PADDING * 2,
|
||||
200
|
||||
);
|
||||
const height = depth * LEVEL_HEIGHT + SVG_PADDING * 2;
|
||||
const pos = calculatePositions(tree, width);
|
||||
return { positions: pos, svgWidth: width, svgHeight: height };
|
||||
}, [tree]);
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col items-center', className)}>
|
||||
{tree.label && (
|
||||
<span className="mb-2 text-sm font-medium text-[var(--muted-foreground)]">
|
||||
{tree.label}
|
||||
</span>
|
||||
)}
|
||||
<svg
|
||||
width={svgWidth}
|
||||
height={svgHeight}
|
||||
viewBox={`0 0 ${svgWidth} ${svgHeight}`}
|
||||
className="overflow-visible"
|
||||
>
|
||||
{/* Draw edges first (behind nodes) */}
|
||||
{positions.map(({ node, x, y, parentX, parentY }) =>
|
||||
parentX !== undefined && parentY !== undefined ? (
|
||||
<motion.line
|
||||
key={`edge-${node.id}`}
|
||||
x1={parentX}
|
||||
y1={parentY + NODE_RADIUS}
|
||||
x2={x}
|
||||
y2={y - NODE_RADIUS}
|
||||
className="stroke-[var(--border)]"
|
||||
strokeWidth={2}
|
||||
initial={false}
|
||||
animate={{
|
||||
opacity: node.state === 'visited' ? 0.5 : 1,
|
||||
}}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
|
||||
{/* Draw nodes */}
|
||||
{positions.map(({ node, x, y }) => (
|
||||
<TreeNode
|
||||
key={node.id}
|
||||
node={node}
|
||||
x={x}
|
||||
y={y}
|
||||
radius={NODE_RADIUS}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Current node indicator arrow */}
|
||||
{currentNodeId && (() => {
|
||||
const currentPos = positions.find((p) => p.node.id === currentNodeId);
|
||||
if (!currentPos) return null;
|
||||
const { x, y } = currentPos;
|
||||
const arrowY = y + NODE_RADIUS + 12;
|
||||
return (
|
||||
<g key={`current-indicator-${currentNodeId}`}>
|
||||
<motion.polygon
|
||||
points={`${x},${arrowY} ${x - 6},${arrowY + 10} ${x + 6},${arrowY + 10}`}
|
||||
className="fill-[var(--primary)]"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
<motion.text
|
||||
x={x}
|
||||
y={arrowY + 24}
|
||||
textAnchor="middle"
|
||||
className="fill-[var(--primary)] text-xs font-medium"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
curr
|
||||
</motion.text>
|
||||
</g>
|
||||
);
|
||||
})()}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { QueueState } from '@/lib/visualizations/types';
|
||||
import { QueueElement } from '../primitives/queue-element';
|
||||
|
||||
interface QueueViewProps {
|
||||
queue: QueueState;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function QueueView({ queue, className }: QueueViewProps) {
|
||||
const hasElements = queue.elements.length > 0;
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col items-center', className)}>
|
||||
{queue.label && (
|
||||
<span className="mb-2 whitespace-nowrap text-sm font-medium text-[var(--muted-foreground)]">
|
||||
{queue.label}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="relative flex min-w-[88px] flex-row items-center gap-1">
|
||||
{/* Empty state */}
|
||||
{!hasElements && (
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg border-2 border-dashed border-[var(--border)] text-xs text-[var(--muted-foreground)]">
|
||||
empty
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Queue elements - horizontal row, front on left, rear on right */}
|
||||
<AnimatePresence mode="popLayout">
|
||||
{queue.elements.map((element, index) => (
|
||||
<QueueElement
|
||||
key={element.id}
|
||||
element={element}
|
||||
isFront={index === 0}
|
||||
isRear={index === queue.elements.length - 1 && queue.elements.length > 1}
|
||||
/>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Direction indicator */}
|
||||
<div className="mt-4 flex items-center gap-2 text-[10px] text-[var(--muted-foreground)]">
|
||||
<span>dequeue</span>
|
||||
<span>←</span>
|
||||
<span className="w-8 border-t border-dashed border-[var(--border)]" />
|
||||
<span>→</span>
|
||||
<span>enqueue</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,14 +12,21 @@ export { CalculationBubble } from "./primitives/calculation-bubble";
|
||||
export { LinkedListNode } from "./primitives/linked-list-node";
|
||||
export { LinkedListPointer } from "./primitives/linked-list-pointer";
|
||||
export { StackElement } from "./primitives/stack-element";
|
||||
export { QueueElement } from "./primitives/queue-element";
|
||||
export { TreeNode } from "./primitives/tree-node";
|
||||
|
||||
// Data structures
|
||||
export { ArrayView } from "./data-structures/array-view";
|
||||
export { LinkedListView } from "./data-structures/linked-list-view";
|
||||
export { StackView } from "./data-structures/stack-view";
|
||||
export { QueueView } from "./data-structures/queue-view";
|
||||
export { BinaryTreeView } from "./data-structures/binary-tree-view";
|
||||
|
||||
// Algorithm visualizations
|
||||
export { MonotonicStackVisualization } from "./algorithms/monotonic-stack";
|
||||
export { PrefixSumVisualization } from "./algorithms/prefix-sum";
|
||||
export { TreeTraversalVisualization } from "./algorithms/tree-traversal";
|
||||
export { BFSVisualization } from "./algorithms/bfs";
|
||||
export { DFSVisualization } from "./algorithms/dfs";
|
||||
export { TwoPointersVisualization } from "./algorithms/two-pointers";
|
||||
export { LinkedListVisualization } from "./algorithms/linked-list";
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { QueueElementState } from '@/lib/visualizations/types';
|
||||
|
||||
interface QueueElementProps {
|
||||
element: QueueElementState;
|
||||
isFront?: boolean;
|
||||
isRear?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const STATE_CLASSES = {
|
||||
normal: 'bg-[var(--surface-variant)] border-[var(--border)] text-[var(--foreground)]',
|
||||
highlighted: 'bg-[var(--primary)]/20 border-[var(--primary)] text-[var(--primary)]',
|
||||
enqueued: 'bg-[var(--success)]/20 border-[var(--success)] text-[var(--success)]',
|
||||
dequeued: 'bg-[var(--error)]/20 border-[var(--error)] text-[var(--error)]',
|
||||
} as const;
|
||||
|
||||
export function QueueElement({
|
||||
element,
|
||||
isFront = false,
|
||||
isRear = false,
|
||||
className,
|
||||
}: QueueElementProps) {
|
||||
return (
|
||||
<motion.div
|
||||
layout
|
||||
initial={{ opacity: 0, x: 20, scale: 0.8 }}
|
||||
animate={{
|
||||
opacity: element.state === 'dequeued' ? 0 : 1,
|
||||
x: 0,
|
||||
scale: element.state === 'highlighted' ? 1.05 : 1,
|
||||
}}
|
||||
exit={{ opacity: 0, x: -20, scale: 0.8 }}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 300,
|
||||
damping: 30,
|
||||
}}
|
||||
className={cn(
|
||||
'relative flex h-10 w-10 items-center justify-center rounded-lg border-2 font-mono font-medium transition-colors duration-200',
|
||||
STATE_CLASSES[element.state],
|
||||
className
|
||||
)}
|
||||
>
|
||||
{element.value}
|
||||
{isFront && (
|
||||
<span className="absolute -bottom-5 text-[10px] font-medium text-[var(--muted-foreground)]">
|
||||
FRONT
|
||||
</span>
|
||||
)}
|
||||
{isRear && (
|
||||
<span className="absolute -bottom-5 text-[10px] font-medium text-[var(--muted-foreground)]">
|
||||
REAR
|
||||
</span>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { BinaryTreeNodeState } from '@/lib/visualizations/types';
|
||||
|
||||
interface TreeNodeProps {
|
||||
node: BinaryTreeNodeState;
|
||||
x: number;
|
||||
y: number;
|
||||
radius?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const STATE_CLASSES = {
|
||||
normal: 'fill-[var(--surface-variant)] stroke-[var(--border)]',
|
||||
current: 'fill-[var(--primary)]/20 stroke-[var(--primary)]',
|
||||
visiting: 'fill-[var(--info)]/20 stroke-[var(--info)]',
|
||||
visited: 'fill-[var(--success)]/20 stroke-[var(--success)] opacity-70',
|
||||
highlighted: 'fill-[var(--primary)]/30 stroke-[var(--primary)]',
|
||||
} as const;
|
||||
|
||||
const TEXT_CLASSES = {
|
||||
normal: 'fill-[var(--foreground)]',
|
||||
current: 'fill-[var(--primary)]',
|
||||
visiting: 'fill-[var(--info)]',
|
||||
visited: 'fill-[var(--success)]',
|
||||
highlighted: 'fill-[var(--primary)]',
|
||||
} as const;
|
||||
|
||||
export function TreeNode({
|
||||
node,
|
||||
x,
|
||||
y,
|
||||
radius = 24,
|
||||
className,
|
||||
}: TreeNodeProps) {
|
||||
const isActive = node.state === 'current' || node.state === 'highlighted';
|
||||
|
||||
return (
|
||||
<motion.g
|
||||
initial={false}
|
||||
animate={{
|
||||
scale: isActive ? 1.1 : 1,
|
||||
}}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 300,
|
||||
damping: 30,
|
||||
}}
|
||||
style={{ transformOrigin: `${x}px ${y}px` }}
|
||||
className={cn(className)}
|
||||
>
|
||||
<motion.circle
|
||||
cx={x}
|
||||
cy={y}
|
||||
r={radius}
|
||||
strokeWidth={2}
|
||||
className={cn(
|
||||
'transition-colors duration-200',
|
||||
STATE_CLASSES[node.state]
|
||||
)}
|
||||
initial={false}
|
||||
animate={{
|
||||
filter: isActive ? 'drop-shadow(0 0 8px var(--primary))' : 'none',
|
||||
}}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
<text
|
||||
x={x}
|
||||
y={y}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
className={cn(
|
||||
'pointer-events-none select-none font-mono text-sm font-medium',
|
||||
TEXT_CLASSES[node.state]
|
||||
)}
|
||||
>
|
||||
{node.value}
|
||||
</text>
|
||||
</motion.g>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user