()
| 157 | } |
| 158 | |
| 159 | function render() { |
| 160 | const sim = simRef.current; |
| 161 | const theme = (themeRef.current ??= readTheme(canvas)); |
| 162 | const rect = sizeRef.current; |
| 163 | const dpr = window.devicePixelRatio || 1; |
| 164 | if (canvas.width !== Math.round(rect.width * dpr) || canvas.height !== Math.round(rect.height * dpr)) { |
| 165 | canvas.width = Math.round(rect.width * dpr); |
| 166 | canvas.height = Math.round(rect.height * dpr); |
| 167 | } |
| 168 | ctx.setTransform(dpr, 0, 0, dpr, 0, 0); |
| 169 | ctx.clearRect(0, 0, rect.width, rect.height); |
| 170 | if (!sim) return; |
| 171 | |
| 172 | const camera = cameraRef.current; |
| 173 | ctx.translate(rect.width / 2, rect.height / 2); |
| 174 | ctx.scale(camera.k, camera.k); |
| 175 | ctx.translate(-camera.x, -camera.y); |
| 176 | |
| 177 | const { selectedId: selId, pathIds: path, focusId: focId } = propsRef.current; |
| 178 | const hover = hoverRef.current; |
| 179 | const highlight = neighborhood(hover); |
| 180 | const pathSet = new Set(path); |
| 181 | const pathPairs = new Set<string>(); |
| 182 | for (let i = 0; i + 1 < path.length; i++) { |
| 183 | pathPairs.add(`${path[i]}>${path[i + 1]}`); |
| 184 | pathPairs.add(`${path[i + 1]}>${path[i]}`); |
| 185 | } |
| 186 | |
| 187 | // --- edges --- |
| 188 | for (const edge of sim.edges) { |
| 189 | const onPath = pathPairs.has(`${edge.source}>${edge.target}`); |
| 190 | const inHighlight = |
| 191 | !highlight || (highlight.has(edge.source) && highlight.has(edge.target)); |
| 192 | const style = edgeStyle(edge.kind, theme); |
| 193 | ctx.globalAlpha = inHighlight || onPath ? 1 : DIM_ALPHA; |
| 194 | ctx.strokeStyle = onPath ? theme.path : style.color; |
| 195 | ctx.lineWidth = (onPath ? 2.4 : style.width) / Math.sqrt(camera.k); |
| 196 | ctx.setLineDash(onPath ? [] : style.dash); |
| 197 | ctx.beginPath(); |
| 198 | ctx.moveTo(edge.sourceNode.x, edge.sourceNode.y); |
| 199 | ctx.lineTo(edge.targetNode.x, edge.targetNode.y); |
| 200 | ctx.stroke(); |
| 201 | ctx.setLineDash([]); |
| 202 | ctx.fillStyle = onPath ? theme.path : style.color; |
| 203 | drawArrow(edge, camera); |
| 204 | } |
| 205 | |
| 206 | // --- nodes --- |
| 207 | for (const node of sim.nodes) { |
| 208 | const isSelected = node.id === selId; |
| 209 | const isHovered = hover ? node.id === hover.id : false; |
| 210 | const onPath = pathSet.has(node.id); |
| 211 | const inHighlight = !highlight || highlight.has(node.id); |
| 212 | ctx.globalAlpha = inHighlight || onPath ? 1 : DIM_ALPHA; |
| 213 | |
| 214 | ctx.beginPath(); |
| 215 | ctx.arc(node.x, node.y, node.radius, 0, Math.PI * 2); |
| 216 | ctx.fillStyle = theme.family[kindFamily(node.kind)] || theme.family.other; |
no test coverage detected