import { useEffect, useId, useRef, useState } from "react"; import type { Action } from "../types"; import "./Screenshot.css"; export interface ScreenshotProps { src?: string; action?: Action; deviceWidth?: number; deviceHeight?: number; } // stepOfSource reads the step out of the screenshot URL the panel is rendering, // so a spec can compare what this panel shows against what the toolbar claims. function stepOfSource(src: string): string | undefined { return /step-0*(\d+)\.png/.exec(src)?.[1]; } const DEFAULT_WIDTH = 1080; const DEFAULT_HEIGHT = 1920; export default function Screenshot({ src, action, deviceWidth, deviceHeight }: ScreenshotProps) { const [naturalSize, setNaturalSize] = useState<{ width: number; height: number } | null>(null); const [loadFailed, setLoadFailed] = useState(false); const previousSrc = useRef(src); const reactId = useId(); const maskId = `screenshot-spotlight-${reactId.replace(/:/g, "")}`; useEffect(() => { if (previousSrc.current !== src) { previousSrc.current = src; setLoadFailed(false); setNaturalSize(null); } }, [src]); if (!src || loadFailed) { return (
no screenshot
); } const width = deviceWidth ?? naturalSize?.width ?? DEFAULT_WIDTH; const height = deviceHeight ?? naturalSize?.height ?? DEFAULT_HEIGHT; const stroke = Math.max(2, width / 200); const tapRadius = Math.max(12, width / 60); const arrowHeadSize = Math.max(18, width / 40); const spotlightRadius = tapRadius * 3; const bounds = action?.resolved_bounds; const tap = action?.tap_point; const isSwipe = action?.kind === "Swipe" && action.from_x !== undefined && action.from_y !== undefined && action.to_x !== undefined && action.to_y !== undefined; return (
device screenshot { const img = event.currentTarget; if (img.naturalWidth > 0 && img.naturalHeight > 0) { setNaturalSize({ width: img.naturalWidth, height: img.naturalHeight }); } }} onError={() => setLoadFailed(true)} />
); } interface SwipeArrowProps { fromX: number; fromY: number; toX: number; toY: number; stroke: number; headSize: number; } function SwipeArrow({ fromX, fromY, toX, toY, stroke, headSize }: SwipeArrowProps) { const angle = (Math.atan2(toY - fromY, toX - fromX) * 180) / Math.PI; const half = headSize / 2; const points = `${toX},${toY} ${toX - headSize},${toY - half} ${toX - headSize},${toY + half}`; return ( ); }