concept-collection / ans-visualizer
ans-visualizer / src / utils / canvasRenderer.ts
199 lines · 4.5 KBBlameHistoryRaw
1import type { StateInfo } from "../types";
3export interface BoxPosition {
4 x: number;
5 y: number;
6 width: number;
7 height: number;
8}
10export interface RenderConfig {
11 boxSize: number;
12 boxesPerRow: number;
13 L: number;
14 canvasWidth: number;
15 canvasHeight: number;
18/**
19 * Calculate the position and dimensions of a state box
20 */
21export function getBoxBounds(
22 stateIndex: number,
23 config: RenderConfig
24): BoxPosition {
25 const { boxSize, boxesPerRow, L } = config;
26 const row = Math.floor(stateIndex / boxesPerRow);
27 const col = stateIndex % boxesPerRow;
28 const boxHeight = Math.floor(boxSize * 0.6);
30 // Calculate group-aware positioning
31 const groupSize = L;
32 const groupIndex = Math.floor(col / groupSize);
33 const gapSize = 6;
35 const x = col * boxSize + groupIndex * gapSize;
36 const y = row * boxHeight;
38 return { x, y, width: boxSize, height: boxHeight };
41/**
42 * Get the center point of a box for edge drawing
43 */
44export function getBoxCenter(
45 stateIndex: number,
46 config: RenderConfig
47): { x: number; y: number } {
48 const bounds = getBoxBounds(stateIndex, config);
49 return {
50 x: bounds.x + bounds.width / 2,
51 y: bounds.y + bounds.height / 2,
52 };
55/**
56 * Find which box (if any) is at the given mouse coordinates
57 */
58export function getBoxAtPosition(
59 mouseX: number,
60 mouseY: number,
61 numStates: number,
62 config: RenderConfig
63): number | null {
64 for (let i = 0; i < numStates; i++) {
65 const bounds = getBoxBounds(i, config);
66 if (
67 mouseX >= bounds.x &&
68 mouseX <= bounds.x + bounds.width &&
69 mouseY >= bounds.y &&
70 mouseY <= bounds.y + bounds.height
71 ) {
72 return i;
73 }
74 }
75 return null;
78/**
79 * Draw a single state box on the canvas
80 */
81export function drawStateBox(
82 ctx: CanvasRenderingContext2D,
83 state: StateInfo,
84 config: RenderConfig,
85 isHovered: boolean = false
86): void {
87 const bounds = getBoxBounds(state.index, config);
88 const { x, y, width, height } = bounds;
90 // Draw box background
91 ctx.fillStyle = state.symbol.color;
92 ctx.fillRect(x, y, width, height);
94 // Draw border
95 ctx.strokeStyle = "rgba(0, 0, 0, 0.1)";
96 ctx.lineWidth = 1;
97 ctx.strokeRect(x, y, width, height);
99 // Draw hover effect
100 if (isHovered) {
101 ctx.strokeStyle = "rgba(0, 0, 0, 0.3)";
102 ctx.lineWidth = 2;
103 ctx.strokeRect(x - 1, y - 1, width + 2, height + 2);
104 }
106 // Draw text
107 ctx.fillStyle = "white";
108 ctx.font = `bold ${config.boxSize > 20 ? 12 : 10}px Arial`;
109 ctx.textAlign = "center";
110 ctx.textBaseline = "middle";
111 ctx.shadowColor = "rgba(0, 0, 0, 0.5)";
112 ctx.shadowBlur = 2;
113 ctx.shadowOffsetX = 0;
114 ctx.shadowOffsetY = 1;
115 ctx.fillText(state.symbol.name, x + width / 2, y + height / 2);
116 ctx.shadowColor = "transparent";
117 ctx.shadowBlur = 0;
120/**
121 * Draw an arrow head at the end of a line
122 */
123function drawArrowHead(
124 ctx: CanvasRenderingContext2D,
125 fromX: number,
126 fromY: number,
127 toX: number,
128 toY: number,
129 color: string
130): void {
131 const headLength = 10;
132 const angle = Math.atan2(toY - fromY, toX - fromX);
134 ctx.save();
135 ctx.fillStyle = color;
136 ctx.beginPath();
137 ctx.moveTo(toX, toY);
138 ctx.lineTo(
139 toX - headLength * Math.cos(angle - Math.PI / 6),
140 toY - headLength * Math.sin(angle - Math.PI / 6)
141 );
142 ctx.lineTo(
143 toX - headLength * Math.cos(angle + Math.PI / 6),
144 toY - headLength * Math.sin(angle + Math.PI / 6)
145 );
146 ctx.closePath();
147 ctx.fill();
148 ctx.restore();
151/**
152 * Draw an edge (line with arrow) between two states
153 */
154export function drawEdge(
155 ctx: CanvasRenderingContext2D,
156 fromState: number,
157 toState: number,
158 config: RenderConfig,
159 color: string,
160 strokeWidth: number = 3
161): void {
162 const fromPos = getBoxCenter(fromState, config);
163 const toPos = getBoxCenter(toState, config);
165 // Draw line
166 ctx.strokeStyle = color;
167 ctx.lineWidth = strokeWidth;
168 ctx.beginPath();
169 ctx.moveTo(fromPos.x, fromPos.y);
170 ctx.lineTo(toPos.x, toPos.y);
171 ctx.stroke();
173 // Draw arrow head
174 drawArrowHead(ctx, fromPos.x, fromPos.y, toPos.x, toPos.y, color);
177/**
178 * Clear the entire canvas
179 */
180export function clearCanvas(
181 ctx: CanvasRenderingContext2D,
182 width: number,
183 height: number
184): void {
185 ctx.clearRect(0, 0, width, height);
188/**
189 * Calculate the total height needed for the canvas
190 */
191export function calculateCanvasHeight(
192 numStates: number,
193 config: RenderConfig
194): number {
195 if (config.boxesPerRow === 0) return 0;
196 const numRows = Math.ceil(numStates / config.boxesPerRow);
197 const boxHeight = Math.floor(config.boxSize * 0.6);
198 return numRows * boxHeight;