/ concept-collection / hitandrun-interactive
Sign in
concept-collection / hitandrun-interactive
hitandrun-interactive / app / src / render / RegionView.tsx
198 lines · 6.0 KBCodeBlameHistory
9fbdc3eInteractive hit-and-run sampling of a 2D convex regionJeremy Magland 1import { useEffect, useRef, type CSSProperties } from "react";
3export interface Points {
4 x: number[];
5 y: number[];
6}
8export interface Segment {
9 x0: number;
10 y0: number;
11 x1: number;
12 y1: number;
15export interface Pt {
16 x: number;
17 y: number;
20interface RegionViewProps {
21 /** Convex region boundary, in counterclockwise world coordinates. */
22 region: Points;
23 /** Sample points to scatter inside the region. */
24 samples: Points;
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 25 /** Animation overlay: the in-region segments the current step samples along
26 * (one for a convex region; possibly several for a non-convex one). */
27 segments?: Segment[] | null;
9fbdc3eInteractive hit-and-run sampling of a 2D convex regionJeremy Magland 28 /** Animation overlay: the current point the step starts from (ringed). */
29 from?: Pt | null;
30 /** Animation overlay: the freshly sampled point (highlighted). */
31 newPoint?: Pt | null;
34const FILL = "rgba(37, 99, 235, 0.08)";
35const STROKE = "#2563eb";
36const DOT = "rgba(15, 23, 42, 0.55)";
37const DOT_RADIUS = 1.6;
38const MARGIN = 28;
40const CHORD = "#f59e0b"; // amber: the candidate segment + its boundary hits
41const PREV = "#d97706"; // deeper amber dot: the sample the chord starts from
42const NEW = "#0f172a"; // near-black: the freshly sampled point (circled)
44/** Renders the region outline and the samples on a 2D canvas, fitting the
45 * region to the available space (aspect-ratio preserved, y pointing up). The
46 * optional `chord` / `from` / `newPoint` overlay drives the sampling movie.
47 * The canvas is redrawn on data change and on resize, and is devicePixelRatio
48 * aware so dots and edges stay crisp. */
49export function RegionView({
50 region,
51 samples,
54 newPoint,
55}: RegionViewProps) {
56 const canvasRef = useRef<HTMLCanvasElement>(null);
57 const containerRef = useRef<HTMLDivElement>(null);
59 useEffect(() => {
60 const canvas = canvasRef.current;
61 const container = containerRef.current;
62 if (!canvas || !container) return;
64 const draw = () => {
65 const ctx = canvas.getContext("2d");
66 if (!ctx) return;
68 const dpr = window.devicePixelRatio || 1;
69 const cssW = container.clientWidth;
70 const cssH = container.clientHeight;
71 if (cssW === 0 || cssH === 0) return;
73 canvas.width = Math.round(cssW * dpr);
74 canvas.height = Math.round(cssH * dpr);
75 canvas.style.width = `${cssW}px`;
76 canvas.style.height = `${cssH}px`;
77 ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
78 ctx.clearRect(0, 0, cssW, cssH);
80 if (region.x.length < 3) return;
82 // World bounds from the region (samples lie inside it).
83 let minX = Infinity;
84 let maxX = -Infinity;
85 let minY = Infinity;
86 let maxY = -Infinity;
87 for (let i = 0; i < region.x.length; i++) {
88 minX = Math.min(minX, region.x[i]);
89 maxX = Math.max(maxX, region.x[i]);
90 minY = Math.min(minY, region.y[i]);
91 maxY = Math.max(maxY, region.y[i]);
92 }
93 const worldW = maxX - minX || 1;
94 const worldH = maxY - minY || 1;
96 // Fit preserving aspect ratio; center within the margins.
97 const scale = Math.min(
98 (cssW - 2 * MARGIN) / worldW,
99 (cssH - 2 * MARGIN) / worldH
100 );
101 const offX = (cssW - worldW * scale) / 2;
102 const offY = (cssH - worldH * scale) / 2;
103 // World (x right, y up) → pixel (x right, y down).
104 const toPx = (x: number) => offX + (x - minX) * scale;
105 const toPy = (y: number) => cssH - (offY + (y - minY) * scale);
107 // Region: translucent fill + crisp outline.
108 ctx.beginPath();
109 ctx.moveTo(toPx(region.x[0]), toPy(region.y[0]));
110 for (let i = 1; i < region.x.length; i++) {
111 ctx.lineTo(toPx(region.x[i]), toPy(region.y[i]));
112 }
113 ctx.closePath();
114 ctx.fillStyle = FILL;
115 ctx.fill();
116 ctx.lineWidth = 2;
117 ctx.strokeStyle = STROKE;
118 ctx.stroke();
120 // Samples: small filled dots.
121 ctx.fillStyle = DOT;
122 const n = Math.min(samples.x.length, samples.y.length);
123 for (let i = 0; i < n; i++) {
124 const cx = toPx(samples.x[i]);
125 const cy = toPy(samples.y[i]);
126 ctx.beginPath();
127 ctx.arc(cx, cy, DOT_RADIUS, 0, 2 * Math.PI);
128 ctx.fill();
129 }
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 131 // Animation overlay (movie mode): the in-region segment(s) of the line.
132 if (segments) {
133 for (const seg of segments) {
134 const x0 = toPx(seg.x0);
135 const y0 = toPy(seg.y0);
136 const x1 = toPx(seg.x1);
137 const y1 = toPy(seg.y1);
9fbdc3eInteractive hit-and-run sampling of a 2D convex regionJeremy Magland 138 ctx.beginPath();
140 ctx.lineTo(x1, y1);
141 ctx.strokeStyle = CHORD;
142 ctx.lineWidth = 1.5;
143 ctx.setLineDash([4, 3]);
146 // Open circles where the line crosses the region boundary.
147 ctx.lineWidth = 1.25;
148 for (const [ex, ey] of [
149 [x0, y0],
150 [x1, y1],
151 ]) {
152 ctx.beginPath();
153 ctx.arc(ex, ey, 3, 0, 2 * Math.PI);
154 ctx.stroke();
155 }
157 }
158 // Previous sample: the point the chord starts from — a filled amber dot.
159 if (from) {
160 ctx.beginPath();
161 ctx.arc(toPx(from.x), toPy(from.y), 4, 0, 2 * Math.PI);
162 ctx.fillStyle = PREV;
163 ctx.fill();
164 }
165 // New sample: a circled black point.
166 if (newPoint) {
167 const cx = toPx(newPoint.x);
168 const cy = toPy(newPoint.y);
169 ctx.beginPath();
170 ctx.arc(cx, cy, 6.5, 0, 2 * Math.PI);
171 ctx.strokeStyle = NEW;
172 ctx.lineWidth = 1.5;
173 ctx.stroke();
174 ctx.beginPath();
175 ctx.arc(cx, cy, 3.2, 0, 2 * Math.PI);
176 ctx.fillStyle = NEW;
177 ctx.fill();
178 }
179 };
181 draw();
182 const ro = new ResizeObserver(draw);
183 ro.observe(container);
184 return () => ro.disconnect();
d9d003fAdd non-convex (star) regions with a general hit-and-run samplerJeremy Magland 185 }, [region, samples, segments, from, newPoint]);
187 return (
188 <div ref={containerRef} style={containerStyle}>
189 <canvas ref={canvasRef} style={{ display: "block" }} />
190 </div>
191 );
194const containerStyle: CSSProperties = {
195 position: "absolute",
196 inset: 0,
197 overflow: "hidden",
198};
moveopenescclose