Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | /** * Viewport utilities for SVG coordinate calculations * * These functions handle the conversion between screen coordinates * and SVG viewBox coordinates, accounting for preserveAspectRatio * letterboxing. */ import type { CursorPosition, SVGPosition, ViewBoxComponents, ViewportInfo } from './types' /** * Parse a viewBox string into its components * * @param viewBox - SVG viewBox string (e.g., "0 0 1000 500") * @returns Parsed components with defaults for missing values */ export function parseViewBox(viewBox: string): ViewBoxComponents { const parts = viewBox.split(' ').map(Number) return { x: parts[0] || 0, y: parts[1] || 0, width: parts[2] || 1000, height: parts[3] || 1000, } } /** * Calculate the actual rendered viewport dimensions accounting for letterboxing. * * When an SVG uses preserveAspectRatio="xMidYMid meet" (the default), the viewBox * content is scaled uniformly to fit within the SVG element, potentially creating * letterboxing (empty space on sides or top/bottom). * * @param svgRect - Bounding rect of the SVG element * @param viewBoxX - ViewBox X origin * @param viewBoxY - ViewBox Y origin * @param viewBoxWidth - ViewBox width * @param viewBoxHeight - ViewBox height * @returns Viewport info including letterbox offsets and scale */ export function getRenderedViewport( svgRect: DOMRect, viewBoxX: number, viewBoxY: number, viewBoxWidth: number, viewBoxHeight: number ): ViewportInfo { const svgAspect = svgRect.width / svgRect.height const viewBoxAspect = viewBoxWidth / viewBoxHeight let renderedWidth: number let renderedHeight: number let letterboxX: number let letterboxY: number if (svgAspect > viewBoxAspect) { // SVG element is wider than viewBox - letterboxing on sides renderedHeight = svgRect.height renderedWidth = renderedHeight * viewBoxAspect letterboxX = (svgRect.width - renderedWidth) / 2 letterboxY = 0 } else { // SVG element is taller than viewBox - letterboxing on top/bottom renderedWidth = svgRect.width renderedHeight = renderedWidth / viewBoxAspect letterboxX = 0 letterboxY = (svgRect.height - renderedHeight) / 2 } // Scale factor is uniform (same for X and Y due to preserveAspectRatio) const scale = renderedWidth / viewBoxWidth return { renderedWidth, renderedHeight, letterboxX, letterboxY, scale, viewBoxX, viewBoxY, } } /** * Convert screen/container coordinates to SVG viewBox coordinates * * @param cursorPosition - Position in container coordinates (pixels) * @param containerRect - Container element's bounding rect * @param svgRect - SVG element's bounding rect * @param viewBox - Parsed viewBox components * @returns Position in SVG coordinate space */ export function screenToSVG( cursorPosition: CursorPosition, containerRect: DOMRect, svgRect: DOMRect, viewBox: ViewBoxComponents ): SVGPosition { const viewport = getRenderedViewport(svgRect, viewBox.x, viewBox.y, viewBox.width, viewBox.height) // Calculate offset from container origin to SVG rendered content const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX const svgOffsetY = svgRect.top - containerRect.top + viewport.letterboxY // Convert screen position to SVG coordinates const svgX = (cursorPosition.x - svgOffsetX) / viewport.scale + viewBox.x const svgY = (cursorPosition.y - svgOffsetY) / viewport.scale + viewBox.y return { svgX, svgY } } /** * Convert SVG viewBox coordinates to screen/container coordinates * * @param svgPosition - Position in SVG coordinate space * @param containerRect - Container element's bounding rect * @param svgRect - SVG element's bounding rect * @param viewBox - Parsed viewBox components * @returns Position in container coordinates (pixels) */ export function svgToScreen( svgPosition: SVGPosition, containerRect: DOMRect, svgRect: DOMRect, viewBox: ViewBoxComponents ): CursorPosition { const viewport = getRenderedViewport(svgRect, viewBox.x, viewBox.y, viewBox.width, viewBox.height) // Calculate offset from container origin to SVG rendered content const svgOffsetX = svgRect.left - containerRect.left + viewport.letterboxX const svgOffsetY = svgRect.top - containerRect.top + viewport.letterboxY // Convert SVG coordinates to screen position const x = (svgPosition.svgX - viewBox.x) * viewport.scale + svgOffsetX const y = (svgPosition.svgY - viewBox.y) * viewport.scale + svgOffsetY return { x, y } } /** * Get viewport info from refs and viewBox string * * Convenience function that handles the common pattern of getting * viewport info from refs and a viewBox string. * * @param containerRef - Container element ref * @param svgRef - SVG element ref * @param viewBoxString - ViewBox string * @returns ViewportInfo or null if refs not available */ export function getViewportFromRefs( containerRef: React.RefObject<HTMLDivElement>, svgRef: React.RefObject<SVGSVGElement>, viewBoxString: string ): ViewportInfo | null { if (!containerRef.current || !svgRef.current) { return null } const svgRect = svgRef.current.getBoundingClientRect() const viewBox = parseViewBox(viewBoxString) return getRenderedViewport(svgRect, viewBox.x, viewBox.y, viewBox.width, viewBox.height) } /** * Calculate the leftover rectangle dimensions (area not covered by safe zones) * * @param containerWidth - Container width in pixels * @param containerHeight - Container height in pixels * @param margins - Safe zone margins * @returns Width and height of the leftover area */ export function getLeftoverDimensions( containerWidth: number, containerHeight: number, margins: { top: number; right: number; bottom: number; left: number } ): { width: number; height: number } { return { width: containerWidth - margins.left - margins.right, height: containerHeight - margins.top - margins.bottom, } } |