| 1 | --- |
| 2 | /** |
| 3 | * Section divider — hairlines tapering toward a centered geometric |
| 4 | * glyph, with small flanking dots for the 17th-c. engraved-rule feel. |
| 5 | * |
| 6 | * Glyphs are mathematically perfect regular polygons (or a line / a |
| 7 | * circle as the limit cases). The home page uses these to compose |
| 8 | * a "platonic descent" — line → triangle → square → pentagon → |
| 9 | * hexagon → heptagon → octagon → circle — down the page. |
| 10 | */ |
| 11 | |
| 12 | type Variant = |
| 13 | | "line" |
| 14 | | "triangle" |
| 15 | | "square" |
| 16 | | "pentagon" |
| 17 | | "hexagon" |
| 18 | | "heptagon" |
| 19 | | "octagon" |
| 20 | | "circle"; |
| 21 | |
| 22 | interface PolygonSpec { |
| 23 | /** number of sides; 0 means special-cased (line / circle) */ |
| 24 | sides: number; |
| 25 | /** rotation of the first vertex in degrees (SVG: 0° = right, 90° = down) */ |
| 26 | startDeg: number; |
| 27 | } |
| 28 | |
| 29 | const POLYGON: Record<Variant, PolygonSpec> = { |
| 30 | line: { sides: 0, startDeg: 0 }, |
| 31 | triangle: { sides: 3, startDeg: -90 }, // apex up |
| 32 | square: { sides: 4, startDeg: -45 }, // sides axis-aligned |
| 33 | pentagon: { sides: 5, startDeg: -90 }, // apex up |
| 34 | hexagon: { sides: 6, startDeg: 0 }, // flat-top |
| 35 | heptagon: { sides: 7, startDeg: -90 }, // apex up |
| 36 | octagon: { sides: 8, startDeg: -67.5 }, // flat-top |
| 37 | circle: { sides: 0, startDeg: 0 }, |
| 38 | }; |
| 39 | |
| 40 | function polygonPoints( |
| 41 | sides: number, |
| 42 | startDeg: number, |
| 43 | cx = 12, |
| 44 | cy = 12, |
| 45 | r = 9.5, |
| 46 | ): string { |
| 47 | const pts: string[] = []; |
| 48 | for (let i = 0; i < sides; i++) { |
| 49 | const angle = ((startDeg + (360 / sides) * i) * Math.PI) / 180; |
| 50 | const x = cx + r * Math.cos(angle); |
| 51 | const y = cy + r * Math.sin(angle); |
| 52 | pts.push(`${x.toFixed(3)},${y.toFixed(3)}`); |
| 53 | } |
| 54 | return pts.join(" "); |
| 55 | } |
| 56 | |
| 57 | interface Props { |
| 58 | variant?: Variant; |
| 59 | size?: number; |
| 60 | width?: string; |
| 61 | class?: string; |
| 62 | } |
| 63 | |
| 64 | const { |
| 65 | variant = "circle", |
| 66 | size = 22, |
| 67 | width = "320px", |
| 68 | class: className, |
| 69 | }: Props = Astro.props; |
| 70 | |
| 71 | const spec = POLYGON[variant as Variant]; |
| 72 | const points = spec.sides >= 3 ? polygonPoints(spec.sides, spec.startDeg) : ""; |
| 73 | --- |
| 74 | |
| 75 | <div |
| 76 | class:list={["alc-divider", className]} |
| 77 | style={`--alc-divider-width:${width};`} |
| 78 | aria-hidden="true" |
| 79 | > |
| 80 | <span class="alc-divider__rule"></span> |
| 81 | <span class="alc-divider__bullet"></span> |
| 82 | <svg |
| 83 | class="alc-divider__shape" |
| 84 | width={size} |
| 85 | height={size} |
| 86 | viewBox="0 0 24 24" |
| 87 | fill="none" |
| 88 | stroke="currentColor" |
| 89 | stroke-width="1.1" |
| 90 | stroke-linecap="round" |
| 91 | stroke-linejoin="miter" |
| 92 | stroke-miterlimit="6" |
| 93 | > |
| 94 | {variant === "line" && <line x1="2" y1="12" x2="22" y2="12" />} |
| 95 | {variant === "circle" && <circle cx="12" cy="12" r="9.5" />} |
| 96 | {points !== "" && <polygon points={points} />} |
| 97 | </svg> |
| 98 | <span class="alc-divider__bullet"></span> |
| 99 | <span class="alc-divider__rule"></span> |
| 100 | </div> |
| 101 | |
| 102 | <style> |
| 103 | .alc-divider { |
| 104 | display: flex; |
| 105 | align-items: center; |
| 106 | gap: 10px; |
| 107 | margin: 0 auto; |
| 108 | max-width: var(--alc-divider-width, 320px); |
| 109 | padding: 28px 0; |
| 110 | color: var(--alc-fg-3); |
| 111 | opacity: 0.7; |
| 112 | } |
| 113 | .alc-divider__rule { |
| 114 | flex: 1; |
| 115 | height: 1px; |
| 116 | background: linear-gradient( |
| 117 | to right, |
| 118 | transparent, |
| 119 | currentColor 70%, |
| 120 | currentColor |
| 121 | ); |
| 122 | } |
| 123 | .alc-divider__rule:last-child { |
| 124 | background: linear-gradient( |
| 125 | to left, |
| 126 | transparent, |
| 127 | currentColor 70%, |
| 128 | currentColor |
| 129 | ); |
| 130 | } |
| 131 | .alc-divider__bullet { |
| 132 | flex: none; |
| 133 | width: 3px; |
| 134 | height: 3px; |
| 135 | border-radius: 50%; |
| 136 | background: currentColor; |
| 137 | } |
| 138 | .alc-divider__shape { |
| 139 | flex: none; |
| 140 | display: block; |
| 141 | } |
| 142 | </style> |