Search code examples
javascriptcssmathsvg

Architecture to draw an SVG polygon with rounded corners, which can scale to different sizes and yet keep same corner rounding?


Right now I have this polygon code:

import { useMemo } from 'react'

export const SIZE = 2048

export const CENTER = SIZE / 2

const ANGLE = -Math.PI / 2 // Start the first vertex at the top center

type PolygonInput = {
  sides: number
  stroke?: string
  strokeWidth?: number
  fill?: string
  rotation?: number // New prop for rotation in degrees
}

export function computePolygonPoints({
  radius,
  centerX,
  centerY,
  sides,
}: {
  radius: number
  centerX: number
  centerY: number
  sides: number
}) {
  return Array.from({ length: sides }, (_, i) => {
    const angle = (i * 2 * Math.PI) / sides + ANGLE
    const x = centerX + radius * Math.cos(angle)
    const y = centerY + radius * Math.sin(angle)
    return { x, y }
  })
}

const Polygon: React.FC<PolygonInput> = ({
  sides,
  stroke = 'black',
  strokeWidth = 0,
  fill = 'black',
  rotation = 0, // Default rotation is 0 degrees
}) => {
  if (sides < 3 || sides > 360) {
    throw new Error('Number of sides must be between 3 and 360')
  }

  const points = useMemo(() => {
    const points = computePolygonPoints({
      radius: (SIZE - strokeWidth) / 2,
      centerX: CENTER,
      centerY: CENTER,
      sides,
    })
    return points.map(p => `${p.x},${p.y}`).join(' ')
  }, [sides])

  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      width="100%"
      height="100%"
      viewBox="0 0 2048 2048"
    >
      <g transform={`rotate(${rotation}, ${CENTER}, ${CENTER})`}>
        <polygon
          points={points}
          stroke={stroke}
          strokeWidth={strokeWidth}
          fill={fill}
          strokeLinejoin="round"
        />
      </g>
    </svg>
  )
}

export default Polygon

I use it like this:

<Polygon
  sides={3}
  strokeWidth={8}
  stroke="red"
/>

I wrap it in a <div> with an explicit height/width, giving the polygon a size.

However, the stroke width is proportionally scaled down to however small the polygon is, so given my 2048 dimensions, when the polygon is 256x256px, you can barely see the stroke at 8px strokeWidth. How can I architect this so that the stroke width in my component stays the props.strokeWidth value (in pixels), regardless of the width/height of the final rendered SVG?

It seems I need to do something like this, but having trouble thinking this fully through.

  • Wrap each SVG component in like a Box component, which uses observers to get the latest computed width/height for the wrapping div.
  • Pass that w/h to the SVG.
  • Don't use viewBox in the SVG.
  • Set the w/h of the SVG to the bounding box w/h.
  • Compute the polygon based on the w/h
  • Set strokeWidth to 8px.

But this would mean I can't let the SVG determine the aspect ratio of its own bounding box? Or say I had a 3:5 aspect ratio SVG, I should only set the width or height on the containing Box, and then use the aspect ratio to determine the SVG bounds? And set box.style.width = fit-content or something like that?

What is your system for handling this?

  • Keep stroke width constant no matter the final SVG size.
  • Scale the polygon.

Solution

  • There is a property to specify that you do not want your strokes to scale in width. More info here and here.

    vector-effect: non-scaling-stroke